home *** CD-ROM | disk | FTP | other *** search
/ Shareware Grab Bag / Shareware Grab Bag.iso / 090 / aztcmtsk.arc / AZTASK.DOC < prev    next >
Text File  |  1986-03-24  |  23KB  |  539 lines

  1.            CONCURRENCY AND AZTEC C (8080/Z80)
  2.          Copyright 1986 David E. Cortesi
  3.  
  4.      In DDJ #113 (March 1986), Ernest Bergmann describes in
  5. "Concurrency and Turbo Pascal" a simple method of building a
  6. multitasking program under Borland's Turbo Pascal.
  7.      Turbo Pascal gave Bergmann easy access to the system's stack
  8. pointer, which made his work easy.  I wondered if the same thing
  9. could be done in C, specifically in the Manx AZTEC C for the 8080
  10. which is all I had use of.  The answer is, yes it could, and I
  11. did it.  This is a documentation file for it.
  12.  
  13. ----- Simple multitasking
  14.  
  15.      What Bergmann demonstrates is the use of independent,
  16. cooperating tasks, dispatched under a nonpreemptive, voluntary,
  17. round-robin discipline.  Taking those words one at a time:
  18.      Independent tasks -- each functional unit of the program
  19. executes independently and asynchronously from all other units.
  20. This is a powerful mental tool for program design; it enforces
  21. clean, watertight separation of jobs to modules.  However, the
  22. asynchonicity implies that there will have to be some mechanism
  23. by which tasks can get in touch with each other and synchronize
  24. their efforts.    Bergmann demonstrates a good mechanism, the FIFO
  25. queue.
  26.      Cooperating tasks -- the program units have to be written
  27. with the knowledge that they will be used together, and they have
  28. to follow certain rules for the program as a whole to work.
  29. Contrast this with a general multitasking operating system like
  30. UNIX, under which you can run any mix of programs simultaneously
  31. and they don't have to be aware of each other.
  32.      Nonpreemptive discipline -- in the general operating system,
  33. the system itself can seize control from the task currently
  34. running, and give the CPU to another task.  That's a preemptive
  35. scheduling discipline, under which the scheduler can preempt one
  36. task in favor of another (on an interrupt, for example).
  37. Bergmann demonstrates a nonpreemptive discipline, in which a task
  38. gives up control only when it is ready to.
  39.      Voluntary discipline -- each task gives up control at
  40. specified points in its code, by calling a procedure named
  41. "defer" (in order to "defer" to its fellow tasks).  If a task
  42. runs a long time without defer-ing, its sibling tasks will be
  43. starved for CPU time.  If a task gets hung up waiting for input,
  44. the whole program hangs with it.
  45.      Round-robin scheduling -- the tasks are kept in a list which
  46. is never reordered, and calling defer always passes control to
  47. the next task in the list.  Contrast this to a scheduling system
  48. that has some policy for reordering the list, or some more
  49. complicated rule for scanning it than merely going around and
  50. around.  Such schemes are "prioritized" disciplines.
  51.      In Bergmann's demonstration, every task always executes on
  52. its turn.  It is possible to enhance round-robin scheduling with
  53. a method of "blocking" a task, so that it is skipped on its turn.
  54.  
  55.      In its simplicity, this style of multitasking has a strong
  56. resemblance to design with coroutines.    A call to defer is like a
  57. coroutine linkage, except that it doesn't state which coroutine
  58. is to be called -- the round-robin scheduling list determines
  59. that, and it's external to the code of a task.
  60.      Despite its simplicity, the scheme is a powerful design
  61. tool.  For one example, I think that the natural organization for
  62. a "modem" program is as a family of cooperating tasks linked by
  63. fifo queues.  One task minds the modem input, one the keyboard,
  64. one the screen, one the modem output, one the printer -- and a
  65. central task acts as a switchboard operator to route data among
  66. them.  Simulations also fall nicely into families of tasks.  Even
  67. if its implementation was too slow or otherwise limited, Bergmann
  68. showed us a great tool for learning and teaching these concepts.
  69. I tried to make the C version as useful.
  70.  
  71. ----- Files for Aztec C Tasking
  72.  
  73.      My implementation of Bergmann's concept is strictly for
  74. Aztec C for the 8080/Z80 (A copyright product of Manx Software).
  75. It depends on 8-bit hardware in that it assumes a pointer and an
  76. unsigned integer are compatible, and it depends on the storage
  77. and stack allocation methods used by the Aztec compiler.
  78. Figuring that tasking in any other compiler would require
  79. similar, but unique, fudges, I made no attempt to flag these
  80. dependencies.
  81.      This compiler doesn't support the "void" storage class, UNIX
  82. standard storage allocation functions, or the stronger
  83. type-checking of newer C compilers.  I've tried to indicate in
  84. the code where these things should be used if they are available.
  85.  
  86.      The tasking implementation is in these files:
  87.  
  88.     AZTASK.DOC -- this file
  89.     AZTASK.TSC -- the source of TASKING.C, the code
  90.     AZTASK.TSH -- the source of TASKING.H, the interface
  91.     AZTASK.CRT -- the source of TROOT.C, modified CROOT
  92.  
  93. TASKING.C contains the code of the functions that implement
  94. tasking and queues.  These functions are documented below.  It
  95. also contains a revision of the alloc() function, since the
  96. original didn't work with tasking.
  97.      TASKING.H is an include file that contains definitions of
  98. three data structures, Task, Queue, and Event (initial caps on
  99. each structure name), and extern declarations of all the
  100. functions in TASKING.C.
  101.      TROOT.C is a modification of the original CROOT.C.  For
  102. those unused to C practice, the function croot() is the real
  103. beginning of any C program.  It has the code to parse the command
  104. line into tokens, set up "argc" and "argv," and call the function
  105. main().  Its source is normally given out with any compiler so
  106. that you may alter the way the environment is set up for a
  107. program.
  108.      This CROOT has been modified in several ways: with
  109. improvements (suggested by Allen Holub) in the way it handles the
  110. CP/M command line and with a fix for the poor performance of
  111. redirected stdout files.  And here it has been altered so that,
  112. instead of CALLING your main() function, it makes a TASK of it.
  113. The main() function is initially the only task in the round-robin
  114. list, and croot() kicks it off by calling defer().
  115.  
  116. ----- Linking and Debugging
  117.  
  118.      You must have Aztec C for the 8080/Z80 in order to use these
  119. files.    Download them to their proper names as indicated above.
  120. Compile and assemble TROOT.C and TASKING.C, producing TROOT.O and
  121. TASKING.O.
  122.      Then, when you have a tasking program ready (the sample that
  123. follows, for instance), link it with a command like
  124.  
  125.     ln -t myprog.o troot.o tasking.o libc.lib
  126.  
  127. By naming TROOT.O and TASKING.O before the standard library, you
  128. ensure that the modified croot(), alloc(), and the tasking and
  129. queueing functions are linked.    The -t option creates a symbol
  130. table.    I find that if it is forced to uppercase --
  131.  
  132.     pip myprog.sym=myprog.sym[u]
  133.  
  134. -- it can be used with the SID debugger, which is VERY helpful
  135. for tasking problems:
  136.  
  137.     sid myprog.com myprog.sym
  138.  
  139. When debugging an Aztec C program with SID, keep in mind that
  140. every compiled function commences with a call to a stack setup
  141. routine followed by an inline parameter:
  142.  
  143.     CALL START
  144.     DW   size-of-locals-often-0x0000
  145.     first instruction of function
  146.  
  147. Set pass points and break points 5 bytes after the name of a
  148. function to stop on its first instruction.  Remember also that
  149. the compiler appends an underbar to each function name less than
  150. 8 bytes long.  Here's how to get to the start of the main()
  151. function, then set a pass point in defer().
  152.  
  153.     sid myprog.com myprog.sym
  154.     -i [tokens for command line here]
  155.     -g,.main_+5
  156.     (stop display at start of main)
  157.     -p,.defer_+5
  158.  
  159. The following example program demonstrates the simplest use of
  160. queues and tasks.  (See beyond it for the documentation of the
  161. called functions.)
  162.  
  163. #include "stdio.h"    /* for printf */
  164. #include "tasking.h"    /* interface to tasking and queues */
  165. #define NUM 5        /* test iterations */
  166.  
  167. int rdrfun();          /* forward declaration of task functions,*/
  168. int wrtfun();          /* so they can be named to maketask() */
  169.  
  170. main(argc,argv) int argc; char *argv[];
  171. {
  172. struct Task *rdr, *wrt;
  173. struct Queue *bfr;
  174.  
  175.     bfr = qmake(NUM); /* experiment with NUM-1, and 1 */
  176.  
  177.     /* experiment with reversing this order */
  178.     wrt = maketask(wrtfun,256,1,bfr);
  179.     rdr = maketask(rdrfun,256,1,bfr);
  180.  
  181.     printf("rdr Task  %04x\n",rdr);
  182.     printf("wrt Task  %04x\n",wrt);
  183.     printf("main task ending\n");
  184.     endtask();    /* explicit endtask call */
  185. }
  186.  
  187. rdrfun(bfr) struct Queue *bfr;
  188. {
  189. int j;
  190.     printf("reader started\n");
  191.     do
  192.     {
  193.         j = qget(bfr);
  194.         printf("reading %d\n",j);
  195.     } while (j<NUM);
  196.     printf("reader function exiting\n");
  197.     return; /* endtask is automatic */
  198. }
  199.  
  200. wrtfun(bfr) struct Queue *bfr;
  201. {
  202. int j;
  203.     printf("writer started\n");
  204.     for(j = 0; j<=NUM ; ++j)
  205.     {
  206.         printf("writing %d, ",j);
  207.         qput(bfr,j);
  208.         printf("..and deferring\n");
  209.     /* experiment with removing this defer and with */
  210.     /* making it conditional on odd(j), etc. */
  211.         defer();
  212.     }
  213.     printf("writer function exiting\n");
  214. }
  215.  
  216. ----- The Tasking functions
  217.  
  218. A "task" consists of a unit of code plus the present state of its
  219. variables.  The code of a task is specifically a single function
  220. and the functions it calls.  Its execution state is maintained in
  221. a call-stack as usual, but the stack will be more limited in size
  222. than usual in a C program.  Tasks are created by maketask() --
  223.  
  224.     struct Task *maketask(func,size,narg [,parms...])
  225.         int (*func)(), size, narg;
  226.  
  227. The first argument is the address of the function that will
  228. execute as this task.  It will be entered at its first statement
  229. the next time defer() is called.
  230.  
  231. The second argument is the number of words that "func" needs in
  232. its stack.  This depends on the number of local variables it (and
  233. the functions it calls) uses.  THERE ARE NO STACK-OVERFLOW
  234. CHECKS!  512 words, a kilobyte, is probably sufficient, but by
  235. all means use more if you like.
  236.  
  237. The third argument is the count of parameters to be passed to
  238. "func," while the parameters themselves follow.  The example
  239. program above shows making two tasks each with one parameter.
  240. For another example, croot() kicks off the function main() with
  241. the following statements:
  242.  
  243.     extern int main();
  244.     maketask(&main,256,2,argc,argv);
  245.     defer();
  246.  
  247. Any task may create other tasks, and any function may be started
  248. off as a task as many times as you like.  Each new task is
  249. inserted into the round-robin list immediately following the task
  250. that creates it, and thus immediately ahead of its siblings.
  251. This order can affect the behavior of a program (experiment with
  252. the example program to see).  For instance, if two tasks are
  253. taking items from the same queue, the task that comes first in
  254. the list can starve the one that comes second.
  255.  
  256.      Once started, a task will end and be removed from the list
  257. in one of three ways.  It may end itself with a call to --
  258.  
  259.     void endtask()
  260.  
  261. from which there is no return.    Or it may simply return, because
  262. the address of endtask() is pushed onto its initial stack by
  263. maketask().  Or its creator (or any other task that has its
  264. address) may kill it with --
  265.  
  266.     void killtask(adt)
  267.         struct Task *adt;
  268.  
  269. All of these have the same effect: the task is marked and the
  270. next time defer() reaches it, it is dropped from the dispatch
  271. list.  When no live tasks remain, the program ends and returns to
  272. CP/M.
  273.  
  274.      Each task must voluntarily give up control, and do it often.
  275. This can be done with a call to --
  276.  
  277.     void defer()
  278.  
  279. The defer() function saves the stack pointer of the current task
  280. and then scans along the round-robin list to find the next task
  281. that is ready to run.  (Tasks can be sleeping or blocked, see
  282. below.)  If NO task is ready to run, the program is in deadlock,
  283. and defer() prints a message on stderr and ends the program.  If
  284. it finds a dead task, it removes it from the list.
  285.      Usually it will find a task to run.  It makes that task the
  286. current task, loads its saved stack pointer into the hardware SP,
  287. and returns to it.  In the case of a new task, it's as if there
  288. was a call to defer() immediately before the actual first
  289. statement of the task function.  In all other cases, there was an
  290. actual call to defer() from which control now returns.    Think of
  291. "defer" as a subroutine that contains the code of every other
  292. task; you call it so they can run, then it returns and you run
  293. again.
  294.      The qget() function (below) also contains a call to defer().
  295. A call to one of these functions MUST appear in the central loop
  296. of every task, or the program will bog down.  If tasks don't
  297. voluntarily give up control very frequently (every tenth of a
  298. second or so) the illusion of simultaneous execution breaks down.
  299. If any task lets itself get hung up on input from the keyboard,
  300. or any other operation with an unpredictable delay time, all
  301. other tasks will be hung up while it is hung up.
  302.  
  303.      In some cases it may be useful for a task to get the address
  304. of its own Task structure.  This is returned by --
  305.  
  306.     struct Task *myTask()
  307.  
  308.      In some applications there may be a need to put a task to
  309. sleep.    This can be done with --
  310.  
  311.     void tsleep(adt) struct Task *adt
  312.  
  313. which marks the task *adt as sleeping.    Then defer() will simply
  314. skip over it until it has been awakened with --
  315.  
  316.     void twaken(adt) struct Task *adt
  317.  
  318. One possible use of these functions is with a timer task.  A task
  319. could pass its own Task to a task that managed a timer, along
  320. with a delay value.  The timer task could then tsleep() its
  321. client, wait until the delay had elapsed, and twaken() it.
  322.  
  323.      The status of a task can be checked with --
  324.  
  325.     int tactive(adt) struct Task *adt
  326.  
  327. which returns 1 if the named task is active (would be able to run
  328. the next time defer() gets to it) and 0 otherwise.  Additional
  329. information can be gotten from --
  330.  
  331.     unsigned tinact(adt) struct Task *adt
  332.  
  333. which returns 0 if the task is active, 0x0001 if it has been put
  334. to sleep by tsleep(), 0xffff if it has been killed or ended, and
  335. some other nonzero value if it is blocked on a get or put to a
  336. queue.    Quick quiz: what value will the expression tinact(myTask)
  337. always have and why?
  338.  
  339. ----- The FIFO-Queue Functions
  340.  
  341. A set of cooperating tasks usually needs some way of
  342. communicating data from one task to another.  Since they may run
  343. at different speeds, they usually need some way of buffering data
  344. between them so that the sender of data can temporarily produce
  345. it faster than the consumer consumes it.  These needs are nicely
  346. met by the FIFO queue.    I have included with the tasking
  347. functions a set of functions for managing fifo queues of unsigned
  348. words.
  349.      Although a Queue structure is defined in TASKING.H, a Queue
  350. cannot be declared statically; it must be allocated dynamically
  351. by a call on --
  352.  
  353.     struct Queue *qmake(num) int num
  354.  
  355. to create a fifo queue that can contain at most "num" words.  The
  356. number of words that are needed in a queue is the subject of an
  357. interesting branch of mathematics, queueing theory, and
  358. simulations of problems in queueing theory can be built with this
  359. or Bergmann's tasking system.
  360.  
  361. When a task wants to put an item into a queue, it does so with --
  362.  
  363.     void qput(adq,val) struct Queue *adq; unsigned val
  364.  
  365. The value "val" is appended to the queue as its newest item.  If
  366. the queue is presently full, the active task is said to be
  367. "blocked."  It is marked and defer() is called; it will not run
  368. again until some other task does a get from this queue, at which
  369. time the qput will complete and the task will again be active.
  370.      If another task had been blocked on a get from this queue,
  371. it is unblocked and will retrieve "val" the next time defer()
  372. runs it.  If more than one task had been blocked getting from
  373. this queue, all will be unblocked and the next one in round-robin
  374. sequence will succeed in retrieving "val" (the rest will probably
  375. be blocked again).
  376.  
  377. When a task wants to get an item from a queue, it does it with --
  378.  
  379.     unsigned qget(adq) struct Queue *adq
  380.  
  381. The first thing qget() does is call defer().  This is for
  382. simplicity, because typical "consumer" tasks will contain a
  383. qget() call in their central loops and won't have to have a
  384. defer() as well.  It's also for efficiency, since a defer()
  385. before a get increases the likelihood that the supplying task
  386. will have put a value in the queue, thus decreasing the need for
  387. blocking the active task.  (The qput() function doesn't do a
  388. defer; writing tasks have to defer explicitly.)
  389.      After the defer(), the oldest item in the queue is
  390. retrieved.  If the queue is in fact empty, the task is blocked,
  391. that is, marked inactive and defer() is called (again).  The task
  392. will not run again until some other task puts an item into the
  393. queue; then the qget will complete and return a value.
  394.      If a task has been blocked waiting to put an item in this
  395. queue, it is unblocked and will be able to finish its qput()
  396. call.  If more than one task has been blocked on put, all will be
  397. unblocked and the next in round-robin sequence will complete its
  398. put (the rest will probably be blocked again).
  399.  
  400. All this blocking can be avoided by testing the queue with --
  401.  
  402.     int qempty(adq) struct Queue *adq
  403.  
  404. which returns 1 if the queue is empty (qget() would block),
  405. otherwise 0; or with --
  406.  
  407.     int qnotmt(adq) struct Queue *adq
  408.  
  409. the logical inverse of qempty(); or with --
  410.  
  411.     int qfullQ(adq) struct Queue *adq
  412.  
  413. which returns 1 if the queue is full (qput() would block),
  414. otherwise 0; or with --
  415.  
  416.     int qnotfull(adq) struct Queue *adq
  417.  
  418. which is the logical inverse.
  419.      For example, the task that manages the input side of a modem
  420. might want to put each incoming byte into a fifo for the printer
  421. task, provided that the operator has turned on print logging.
  422. However, if the printer is out of paper the printer fifo might
  423. fill up, and the modem-input task mustn't get blocked or it will
  424. lose input data.  Here's how such a task might look:
  425.  
  426.     do
  427.     {
  428.         defer();
  429.         get-modem-status-bits;
  430.     } while (modem-input-not-ready);
  431.     c = get-modem-data-input-byte;
  432.     if (terminalmode)
  433.     {
  434.         qput(screenfifo,c);
  435.         if (disklogging)
  436.             qput(diskfifo,c);
  437.         if (printlogging)
  438.             if (qnotfull(printfifo))
  439.                 qput(printfifo,c);
  440.     }
  441.     else qput(xmodemfifo,c);
  442.  
  443. The task assumes the screen and disk-log tasks can consume data
  444. at least as fast the modem can produce it, but the printer fifo
  445. might fill up and stay full for unpredictable lengths of time.
  446.  
  447. ----- The Synchronizing Functions
  448.  
  449.      Now, often a program will want to pass objects larger than
  450. an unsigned word.  This can be done, of course, by passing
  451. pointers to objects through the queues, but that brings up
  452. problems of synchronization.  Suppose that task A puts the
  453. address of a block of data in a queue for task B to consume.
  454. When can task A safely re-use that block of data?  Answer: it
  455. cannot know.  There's no way of telling when task B will start,
  456. much less finish, working on that block.  The only safe thing to
  457. do is to wait until task B TELLS task A it is finished.
  458.      This is the central problem of synchronization: how can task
  459. B tell task A it's done?  Here are some possibilities.  First,
  460. task A could pass its own Task along with the data, and tsleep()
  461. itself after putting the item in B's fifo.  Task B would be
  462. expected to twaken() task A when the work was done.  This
  463. precludes task A doing anything useful in the meantime.
  464.      Second, the tasks can use an Event as a synchronizer.  An
  465. Event is simply a fifo queue that has room for only one word.
  466. Task A sends the block of data to task B in a different queue,
  467. then attempts to get an item from the Event they share (or A
  468. might have passed the address of the Event along with the data).
  469. Since a qget from an empty queue blocks a task, this is the
  470. equivalent of A's sleeping itself.  When task B has finished,
  471. it will qput an arbitrary value (or a useful return code) into
  472. the Event, so waking up task A.  As with the tsleep() solution,
  473. this precludes task A doing anything useful while task B is
  474. working, but that's inevitable anyway, if their work centers on
  475. sharing a single object (buffer, block, whatever).
  476.      Third, the tasks could agree on the use of a global, static
  477. variable as a synchronizing flag.  Task A would put a 1 in it,
  478. send the block of data, and then spin in a loop:
  479.  
  480.     do {defer();} while(synchro);
  481.  
  482. When task B was done, it would set 0 in the variable.  This allows
  483. task A to do other things besides defer() while it is waiting, but
  484. it adds needless overhead in the fruitless dispatches of task A.
  485.      If their resources are larger, they can adopt a fourth
  486. solution, the "full-duplex" method.  Here they share a pair of
  487. fifo queues.  One, from B to A, contains the addresses of all the
  488. free objects (buffers, whatever), while the other from A to B
  489. contains the addresses of all the ones that A has filled up for
  490. B to empty.  The objects move between the tasks like cars on a
  491. tramline.
  492.      An Event may act to control access to a single resource that
  493. may be used by only one task at a time.  The resource is
  494. represented by a word in the Event.  A task acquires the
  495. resource by getting a word from the Event, and releases it by
  496. putting the word back.    However, in this implementation, tasks
  497. that are waiting for Events (blocked on empty queues) are not
  498. queued up in the order they accessed the queue.  When the
  499. resource is put back in the Event, all tasks that are waiting for
  500. it are unblocked at once.  The one that gets it will be the one
  501. that comes next in round-robin order after the task that released
  502. the resource.  This results in an equitable sharing of a
  503. resource (it will be handed around the the dispatch list like
  504. the gravy boat at the supper table) but not a "fair" one in the
  505. usual sense since the task that receives a resource will not
  506. necessarily be the one that has waited the longest for it.
  507.  
  508. An Event is created by the special function --
  509.  
  510.     struct Event *evmake();
  511.  
  512. which makes a single package Event of 7 words.    A call of
  513. qmake(1) will make a functionally equivalent Event, but a queue's
  514. array of fifo data is allocated separately from its Queue
  515. structure.
  516.      You wait on an event with evwait(), which is merely --
  517.  
  518.     #define evwait(E) qget((E))
  519.  
  520. and post one on which another task is waiting with evpost() --
  521.  
  522.     #define evpost(E) qput((E),1)
  523.  
  524. although an explicit qput with an informative return code value
  525. will often be better.  An event can be tested without waiting on
  526. it with evwait() --
  527.  
  528.     #define posted(E) qnotmt((E))
  529.  
  530. It would be possible to build much more elaborate synchronizing
  531. mechanisms into this same tasking implementation (semaphores,
  532. queued "fair" access to scarce resources, etc), just as it
  533. would be possible to add an element of randomness to the
  534. dispatch logic of defer(), so that tasks would become truly
  535. asynchronous.  I encourage anyone who feels like it, to
  536. experiment with these things.  I'm simply delighted to have
  537. been able to put such a powerful teaching and modelling tool
  538. in your hands so easily and cheaply.
  539.